# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

import base64
import datetime
import logging
import psycopg2
import smtplib
import threading
import re

from collections import defaultdict
from email.utils import formataddr

from odoo import _, api, fields, models
from odoo import tools
from odoo.addons.base.models.ir_mail_server import MailDeliveryException
from odoo.tools.safe_eval import safe_eval

from email import encoders
from email.charset import Charset
from email.header import Header
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import COMMASPACE, formataddr, formatdate, getaddresses, make_msgid
import logging
import re
import smtplib
import threading

import html2text
from odoo.exceptions import except_orm, UserError
from odoo.tools import ustr, pycompat

from binascii import Error as binascii_error
_image_dataurl = re.compile(r'(data:image/[a-z]+?);base64,([a-z0-9+/\n]{3,}=*)\n*([\'"])(?: data-filename="([^"]*)")?', re.I)

_logger = logging.getLogger(__name__)
_test_logger = logging.getLogger('odoo.tests')

SMTP_TIMEOUT = 60

class MailDeliveryException(except_orm):
    """Specific exception subclass for mail delivery errors"""
    def __init__(self, name, value):
        super(MailDeliveryException, self).__init__(name, value)

# Python 3: patch SMTP's internal printer/debugger
def _print_debug(self, *args):
    _logger.debug(' '.join(str(a) for a in args))
smtplib.SMTP._print_debug = _print_debug

# Python 2: replace smtplib's stderr
class WriteToLogger(object):
    def write(self, s):
        _logger.debug(s)
smtplib.stderr = WriteToLogger()

def is_ascii(s):
    return all(ord(cp) < 128 for cp in s)

def encode_header(header_text):
    """Returns an appropriate representation of the given header value,
       suitable for direct assignment as a header value in an
       email.message.Message. RFC2822 assumes that headers contain
       only 7-bit characters, so we ensure it is the case, using
       RFC2047 encoding when needed.

       :param header_text: unicode or utf-8 encoded string with header value
       :rtype: string | email.header.Header
       :return: if ``header_text`` represents a plain ASCII string,
                return the same 7-bit string, otherwise returns an email.header.Header
                that will perform the appropriate RFC2047 encoding of
                non-ASCII values.
    """
    if not header_text:
        return ""
    header_text = ustr(header_text) # FIXME: require unicode higher up?
    if is_ascii(header_text):
        return pycompat.to_native(header_text)
    return Header(header_text, 'utf-8')

def encode_header_param(param_text):
    """Returns an appropriate RFC2047 encoded representation of the given
       header parameter value, suitable for direct assignation as the
       param value (e.g. via Message.set_param() or Message.add_header())
       RFC2822 assumes that headers contain only 7-bit characters,
       so we ensure it is the case, using RFC2047 encoding when needed.

       :param param_text: unicode or utf-8 encoded string with header value
       :rtype: string
       :return: if ``param_text`` represents a plain ASCII string,
                return the same 7-bit string, otherwise returns an
                ASCII string containing the RFC2047 encoded text.
    """
    # For details see the encode_header() method that uses the same logic
    if not param_text:
        return ""
    param_text = ustr(param_text) # FIXME: require unicode higher up?
    if is_ascii(param_text):
        return pycompat.to_native(param_text) # TODO: is that actually necessary?
    return Charset("utf-8").header_encode(param_text)

address_pattern = re.compile(r'([^ ,<@]+@[^> ,]+)')

def extract_rfc2822_addresses(text):
    """Returns a list of valid RFC2822 addresses
       that can be found in ``source``, ignoring
       malformed ones and non-ASCII ones.
    """
    if not text:
        return []
    candidates = address_pattern.findall(ustr(text))
    return [c for c in candidates if is_ascii(c)]


def encode_rfc2822_address_header(header_text):
    """If ``header_text`` contains non-ASCII characters,
       attempts to locate patterns of the form
       ``"Name" <address@domain>`` and replace the
       ``"Name"`` portion by the RFC2047-encoded
       version, preserving the address part untouched.
    """
    def encode_addr(addr):
        name, email = addr
        # If s is a <text string>, then charset is a hint specifying the
        # character set of the characters in the string. The Unicode string
        # will be encoded using the following charsets in order: us-ascii,
        # the charset hint, utf-8. The first character set to not provoke a
        # UnicodeError is used.
        # -> always pass a text string to Header

        # also Header.__str__ in Python 3 "Returns an approximation of the
        # Header as a string, using an unlimited line length.", the old one
        # was "A synonym for Header.encode()." so call encode() directly?
        name = Header(pycompat.to_text(name)).encode()
        # if the from does not follow the (name <addr>),* convention, we might
        # try to encode meaningless strings as address, as getaddresses is naive
        # note it would also fail on real addresses with non-ascii characters
        try:
            return formataddr((name, email))
        except UnicodeEncodeError:
            _logger.warning(_('Failed to encode the address %s\n'
                              'from mail header:\n%s') % addr, header_text)
            return ""

    addresses = getaddresses([pycompat.to_native(ustr(header_text))])
    return COMMASPACE.join(a for a in (encode_addr(addr) for addr in addresses) if a)

class IrMailServer(models.Model):
    """Represents an SMTP server, able to send outgoing emails, with SSL and TLS capabilities."""
    _inherit = "ir.mail_server"

    @api.model
    def _get_default_bounce_address(self):  #这个方法在mail_thread抽象类中用的，创建mail_message是需要设置
        '''Compute the default bounce address.

        The default bounce address is used to set the envelop address if no
        envelop address is provided in the message.  It is formed by properly
        joining the parameters "mail.bounce.alias" and
        "mail.catchall.domain".

        If "mail.bounce.alias" is not set it defaults to "postmaster-odoo".

        If "mail.catchall.domain" is not set, return None.

        '''
        # get_param = self.env['ir.config_parameter'].sudo().get_param
        # postmaster = get_param('mail.bounce.alias', default='postmaster-odoo')
        # domain = get_param('mail.catchall.domain')
        # if postmaster and domain:
        #     return '%s@%s' % (postmaster, domain)
        #dbh 直接返回当前登录用户的邮箱（参考mail_message.py的def _get_default_from(self):）
        if self.env.user.email:
            return formataddr((self.env.user.name, self.env.user.email))
        raise UserError(_("Unable to post message, please configure the sender's email address."))

    @api.multi
    def test_smtp_connection(self):
        for server in self:
            smtp = False
            try:
                smtp = self.connect(mail_server_id=server.id)
                # simulate sending an email from current user's address - without sending it!
                # email_from, email_to = self.env.user.email, 'noreply@odoo.com'
                email_from, email_to = server.sudo().smtp_user, 'noreply@odoo.com' #dbh 需要加sudo，否则一般用户不能访问
                if not email_from:
                    raise UserError(_('Please configure an email on the current user to simulate '
                                      'sending an email message via this outgoing server'))
                # Testing the MAIL FROM step should detect sender filter problems
                (code, repl) = smtp.mail(email_from)
                if code != 250:
                    raise UserError(_('The server refused the sender address (%(email_from)s) '
                                      'with error %(repl)s') % locals())
                # Testing the RCPT TO step should detect most relaying problems
                (code, repl) = smtp.rcpt(email_to)
                if code not in (250, 251):
                    raise UserError(_('The server refused the test recipient (%(email_to)s) '
                                      'with error %(repl)s') % locals())
                # Beginning the DATA step should detect some deferred rejections
                # Can't use self.data() as it would actually send the mail!
                smtp.putcmd("data")
                (code, repl) = smtp.getreply()
                if code != 354:
                    raise UserError(_('The server refused the test connection '
                                      'with error %(repl)s') % locals())
            except UserError as e:
                # let UserErrors (messages) bubble up
                raise e
            except Exception as e:
                raise UserError(_("Connection Test Failed! Here is what we got instead:\n %s") % ustr(e))
            finally:
                try:
                    if smtp:
                        smtp.close()
                except Exception:
                    # ignored, just a consequence of the previous exception
                    pass
        raise UserError(_("Connection Test Succeeded! Everything seems properly set up!"))


class MailMail(models.Model):
    """ Model holding RFC2822 email messages to send. This model also provides
        facilities to queue and send new email messages.  """
    _inherit = 'mail.mail'

    @api.multi
    def _send(self, auto_commit=False, raise_exception=False, smtp_session=None):
        IrMailServer = self.env['ir.mail_server']
        IrAttachment = self.env['ir.attachment']
        for mail_id in self.ids:
            success_pids = []
            failure_type = None
            processing_pid = None
            mail = None
            try:
                mail = self.browse(mail_id)
                if mail.state != 'outgoing':
                    if mail.state != 'exception' and mail.auto_delete:
                        mail.sudo().unlink()
                    continue

                # remove attachments if user send the link with the access_token
                body = mail.body_html or ''
                attachments = mail.attachment_ids
                for link in re.findall(r'/web/(?:content|image)/([0-9]+)', body):
                    attachments = attachments - IrAttachment.browse(int(link))

                # load attachment binary data with a separate read(), as prefetching all
                # `datas` (binary field) could bloat the browse cache, triggerring
                # soft/hard mem limits with temporary data.
                attachments = [(a['datas_fname'], base64.b64decode(a['datas']), a['mimetype'])
                               for a in attachments.sudo().read(['datas_fname', 'datas', 'mimetype'])]

                # specific behavior to customize the send email for notified partners
                email_list = []
                if mail.email_to:
                    email_list.append(mail._send_prepare_values())
                for partner in mail.recipient_ids:
                    values = mail._send_prepare_values(partner=partner)
                    values['partner_id'] = partner
                    email_list.append(values)

                # headers
                headers = {}
                # ICP = self.env['ir.config_parameter'].sudo()
                # bounce_alias = ICP.get_param("mail.bounce.alias")
                # catchall_domain = ICP.get_param("mail.catchall.domain")
                # if bounce_alias and catchall_domain:
                #     if mail.model and mail.res_id:
                #         headers['Return-Path'] = '%s+%d-%s-%d@%s' % (
                #         bounce_alias, mail.id, mail.model, mail.res_id, catchall_domain)
                #     else:
                #         headers['Return-Path'] = '%s+%d@%s' % (bounce_alias, mail.id, catchall_domain)
                headers['Return-Path']=mail.email_from #dbh 将返回的邮件直接设为该邮箱而非odoo

                if mail.headers:
                    try:
                        headers.update(safe_eval(mail.headers)) #这里是odoo文档的标识，以便可以在odoo中直接看到
                    except Exception:
                        pass

                # Writing on the mail object may fail (e.g. lock on user) which
                # would trigger a rollback *after* actually sending the email.
                # To avoid sending twice the same email, provoke the failure earlier
                mail.write({
                    'state': 'exception',
                    'failure_reason': _(
                        'Error without exception. Probably due do sending an email without computed recipients.'),
                })
                # Update notification in a transient exception state to avoid concurrent
                # update in case an email bounces while sending all emails related to current
                # mail record.
                notifs = self.env['mail.notification'].search([
                    ('is_email', '=', True),
                    ('mail_id', 'in', mail.ids),
                    ('email_status', 'not in', ('sent', 'canceled'))
                ])
                if notifs:
                    notif_msg = _(
                        'Error without exception. Probably due do concurrent access update of notification records. Please see with an administrator.')
                    notifs.sudo().write({
                        'email_status': 'exception',
                        'failure_type': 'UNKNOWN',
                        'failure_reason': notif_msg,
                    })

                # build an RFC2822 email.message.Message object and send it without queuing
                res = None
                for email in email_list:
                    msg = IrMailServer.build_email(
                        email_from=mail.email_from,
                        email_to=email.get('email_to'),
                        subject=mail.subject,
                        body=email.get('body'),
                        body_alternative=email.get('body_alternative'),
                        email_cc=tools.email_split(mail.email_cc),
                        reply_to=mail.reply_to,
                        attachments=attachments,
                        message_id=mail.message_id,
                        references=mail.references,
                        object_id=mail.res_id and ('%s-%s' % (mail.res_id, mail.model)),
                        subtype='html',
                        subtype_alternative='plain',
                        headers=headers)
                    processing_pid = email.pop("partner_id", None)
                    try:
                        res = IrMailServer.send_email(
                            msg, mail_server_id=mail.mail_server_id.id, smtp_session=smtp_session)
                        if processing_pid:
                            success_pids.append(processing_pid)
                        processing_pid = None
                    except AssertionError as error:
                        if str(error) == IrMailServer.NO_VALID_RECIPIENT:
                            failure_type = "RECIPIENT"
                            # No valid recipient found for this particular
                            # mail item -> ignore error to avoid blocking
                            # delivery to next recipients, if any. If this is
                            # the only recipient, the mail will show as failed.
                            _logger.info("Ignoring invalid recipients for mail.mail %s: %s",
                                         mail.message_id, email.get('email_to'))
                        else:
                            raise
                if res:  # mail has been sent at least once, no major exception occured
                    mail.write({'state': 'sent', 'message_id': res, 'failure_reason': False})
                    _logger.info('Mail with ID %r and Message-Id %r successfully sent', mail.id, mail.message_id)
                    # /!\ can't use mail.state here, as mail.refresh() will cause an error
                    # see revid:odo@openerp.com-20120622152536-42b2s28lvdv3odyr in 6.1
                mail._postprocess_sent_message(success_pids=success_pids, failure_type=failure_type)
            except MemoryError:
                # prevent catching transient MemoryErrors, bubble up to notify user or abort cron job
                # instead of marking the mail as failed
                _logger.exception(
                    'MemoryError while processing mail with ID %r and Msg-Id %r. Consider raising the --limit-memory-hard startup option',
                    mail.id, mail.message_id)
                # mail status will stay on ongoing since transaction will be rollback
                raise
            except (psycopg2.Error, smtplib.SMTPServerDisconnected):
                # If an error with the database or SMTP session occurs, chances are that the cursor
                # or SMTP session are unusable, causing further errors when trying to save the state.
                _logger.exception(
                    'Exception while processing mail with ID %r and Msg-Id %r.',
                    mail.id, mail.message_id)
                raise
            except Exception as e:
                failure_reason = tools.ustr(e)
                _logger.exception('failed sending mail (id: %s) due to %s', mail.id, failure_reason)
                mail.write({'state': 'exception', 'failure_reason': failure_reason})
                mail._postprocess_sent_message(success_pids=success_pids, failure_reason=failure_reason,
                                               failure_type='UNKNOWN')
                if raise_exception:
                    if isinstance(e, (AssertionError, UnicodeEncodeError)):
                        if isinstance(e, UnicodeEncodeError):
                            value = "Invalid text: %s" % e.object
                        else:
                            # get the args of the original error, wrap into a value and throw a MailDeliveryException
                            # that is an except_orm, with name and value as arguments
                            value = '. '.join(e.args)
                        raise MailDeliveryException(_("Mail Delivery Failed"), value)
                    raise

            if auto_commit is True:
                self._cr.commit()
        return True

class Message(models.Model):
    """ Messages model: system notification (replacing res.log notifications),
        comments (OpenChatter discussion) and incoming emails. """
    _inherit = 'mail.message'

    @api.model
    def create(self, values):
        # coming from mail.js that does not have pid in its values
        if self.env.context.get('default_starred'):
            self = self.with_context({'default_starred_partner_ids': [(4, self.env.user.partner_id.id)]})

        if 'email_from' not in values:  # needed to compute reply_to
            values['email_from'] = self._get_default_from()
        if not values.get('message_id'):
            values['message_id'] = self._get_message_id(values)
        if 'reply_to' not in values:
            values['reply_to'] = self._get_reply_to(values)
        if 'record_name' not in values and 'default_record_name' not in self.env.context:
            values['record_name'] = self._get_record_name(values)

        if 'attachment_ids' not in values:
            values.setdefault('attachment_ids', [])

        if not values.get('mail_server_id'):
            #这个是发送邮件账号必须与smtp_host账号一致情况：
            mail_from = values['email_from'][values['email_from'].index('<') + 1:values['email_from'].index('>')]  # dbh取<>内的邮箱
            mail_server_id = self.env['ir.mail_server'].sudo().search([('smtp_user', '=', mail_from)]).id  #dbh通个这个邮箱找到（需要加sudo，否则一般用户没有权限），对应的ir.mail_server
            values['mail_server_id']=mail_server_id

            #下面是发送邮件的域与smtp_host的域一致情况发送：（国内不行）
            # email_from_host = values['email_from'][values['email_from'].index('@'):]  # dbh取的邮箱域地址
            # smtp_host='smtp'+email_from_host
            # mail_server_id = self.env['ir.mail_server'].sudo().search([('smtp_host', '=', smtp_host)]).id  #dbh找到同样域的smtp_host账号（需要加sudo，否则一般用户没有权限），对应的ir.mail_server
            # values['mail_server_id']=mail_server_id


        # extract base64 images
        if 'body' in values:
            Attachments = self.env['ir.attachment']
            data_to_url = {}

            def base64_to_boundary(match):
                key = match.group(2)
                if not data_to_url.get(key):
                    name = match.group(4) if match.group(4) else 'image%s' % len(data_to_url)
                    try:
                        attachment = Attachments.create({
                            'name': name,
                            'datas': match.group(2),
                            'datas_fname': name,
                            'res_model': values.get('model'),
                            'res_id': values.get('res_id'),
                        })
                    except binascii_error:
                        _logger.warning(
                            "Impossible to create an attachment out of badly formated base64 embedded image. Image has been removed.")
                        return match.group(3)  # group(3) is the url ending single/double quote matched by the regexp
                    else:
                        attachment.generate_access_token()
                        values['attachment_ids'].append((4, attachment.id))
                        data_to_url[key] = ['/web/image/%s?access_token=%s' % (attachment.id, attachment.access_token),
                                            name]
                return '%s%s alt="%s"' % (data_to_url[key][0], match.group(3), data_to_url[key][1])

            values['body'] = _image_dataurl.sub(base64_to_boundary, tools.ustr(values['body']))

        # delegate creation of tracking after the create as sudo to avoid access rights issues
        tracking_values_cmd = values.pop('tracking_value_ids', False)
        message = super(Message, self).create(values)

        if values.get('attachment_ids'):
            message.attachment_ids.check(mode='read')

        if tracking_values_cmd:
            vals_lst = [dict(cmd[2], mail_message_id=message.id) for cmd in tracking_values_cmd if
                        len(cmd) == 3 and cmd[0] == 0]
            other_cmd = [cmd for cmd in tracking_values_cmd if len(cmd) != 3 or cmd[0] != 0]
            if vals_lst:
                self.env['mail.tracking.value'].sudo().create(vals_lst)
            if other_cmd:
                message.sudo().write({'tracking_value_ids': tracking_values_cmd})

        if values.get('model') and values.get('res_id'):
            message._invalidate_documents()

        return message

